package com.zboin.framework.serve;

import com.auth0.jwt.exceptions.InvalidClaimException;
import com.zboin.domain.LoginUser;
import com.zboin.utils.EncryptUtil;
import com.zboin.utils.IpUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * @author zhboom
 * @date 2023/7/31
 * JWT 由三部分组成：header + payload + salt。
 * 这里的实现是 payload 中存储 随机生成的 tokenKey 和 refreshTokenKey，然后使用该 Key，将用户信息存储在自定义的 CacheMap 中
 */
@Slf4j
@Component
public class TokenServe {

    private static final String TOKEN_KEY = "login_user_key";
    private static final String REFRESH_TOKEN_KEY = "refresh_token_key";
    private static final String TOKEN_PREFIX = "Bearer ";
    public static final String CACHE_TOKEN_PREFIX = "login:token:";
    public static final String CACHE_TOKEN_REFRESH_PREFIX = "refresh:token:";
    private static final long MILLIONS_MINUTE = 60 * 1000L;

    @Value("${zboin.token.secret}")
    private String tokenSecret;

    @Value("${zboin.token.header}")
    private String tokenHeader;

    @Value("${zboin.token.expire}")
    private int tokenExpire;

    @Value("${zboin.token.refresh}")
    private int tokenRefresh;

    private final CacheMap<String, LoginUser> cacheMap;

    public TokenServe() {
        cacheMap = new CacheMap<>();
    }

    /**
     * 根据请求 token 获取登录用户信息
     * @return 对应的登录用户信息
     */
    public LoginUser getLoginUser(String token) {
        if (StringUtils.hasText(token)) {
            Claims claims = parseToken(token);
            // 获取当前登录的用户信息
            String userK = (String) claims.get(TOKEN_KEY);
            String tk = getTokenKey(userK);
            return cacheMap.get(tk);
        }
        return null;
    }

    /**
     * 创建 token，并存储登录用户信息
     * @return token
     */
    public String createToken(LoginUser loginUser) {
        String uuid = simpleUuid();
        String refreshUuid = simpleUuid();
        loginUser.setToken(uuid);
        loginUser.setRefreshToken(refreshUuid);

        Map<String, Object> claims = new HashMap<>(2);
        claims.put(TOKEN_KEY, uuid);
        claims.put(REFRESH_TOKEN_KEY, refreshUuid);
        log.debug("token key: {}", uuid);

        setUserAgent(loginUser);
        cacheToken(loginUser);

        return createToken(claims);
    }

    private String simpleUuid() {
        String uuid = UUID.randomUUID().toString();
        return uuid.replaceAll("-", "");
    }

    /**
     * 校验 token 是否正确，并且校验其过期时间
     * @param token token
     *
     */
    public String validToken(String token) {
        Claims claims = parseToken(token);
        // 获取 token key
        String tk = getTokenKey(claims.get(TOKEN_KEY, String.class));
        String rTk = getRefreshTokenKey(claims.get(REFRESH_TOKEN_KEY, String.class));

        // 判断 token 是否存在及是否过期
        if (!cacheMap.containsKey(tk)) {
            if (cacheMap.containsKey(rTk)) {
                LoginUser loginUser = cacheMap.get(rTk);
                // UserUtil.setLoginUser(loginUser);
                cacheMap.remove(rTk);
                return refreshToken(loginUser);
            }
            else {
                throw new InvalidClaimException("token is invalid, please login.");
            }
        }
        return null;
    }

    /**
     * 获取请求中携带的 token
     * @return token
     */
    public String getToken(HttpServletRequest request) {
        String token = request.getHeader(tokenHeader);
        if (StringUtils.hasText(token) && token.startsWith(TOKEN_PREFIX)) {
            token = token.replace(TOKEN_PREFIX, "");
        }
        else {
            return null;
        }
        return token;
    }

    /**
     * 清除缓存的 token 数据
     * @param token token
     */
    public void clearTokenCache(String token) {
        Claims claims = parseToken(token);
        // 清除 token
        String tk = getTokenKey(claims.get(TOKEN_KEY, String.class));
        cacheMap.remove(tk);
        // 清除刷新 token
        String rTk = getRefreshTokenKey(claims.get(REFRESH_TOKEN_KEY, String.class));
        cacheMap.remove(rTk);
    }


    private String refreshToken(LoginUser loginUser) {
        log.info("refresh token for user: [{}]", loginUser.getUsername());
        return createToken(loginUser);
    }

    /**
     * 设置用户代理
     */
    private void setUserAgent(LoginUser loginUser) {
        // 获取请求 request 对象
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = requestAttributes.getRequest();

        String userAgent = request.getHeader("User-Agent");
        loginUser.setUserAgent(userAgent);

        String ipAddr = IpUtils.getIpAddr(request);
        loginUser.setIpAddr(ipAddr);
    }

    /**
     * 存储 token
     * @param loginUser 登录用户信息
     */
    private void cacheToken(LoginUser loginUser) {
        loginUser.setExpireTime(System.currentTimeMillis() + tokenExpire * MILLIONS_MINUTE);
        String tk = getTokenKey(loginUser.getToken());
        if (cacheMap.containsKey(tk)) {
            cacheMap.remove(tk);
        }
        cacheMap.put(tk, loginUser, tokenExpire);
        // 存储 refreshToken
        String rTk = getRefreshTokenKey(loginUser.getRefreshToken());
        if (cacheMap.containsKey(rTk)) {
            cacheMap.remove(rTk);
        }
        cacheMap.put(rTk, loginUser, tokenRefresh);
        log.debug("put login user info into redis. {}: {}", tk, loginUser);
    }


    private String createToken(Map<String, Object> claims) {
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS256, EncryptUtil.base64Encode(tokenSecret))
                .compact();
        log.debug("create token: {}", token);
        return token;
    }

    private Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(EncryptUtil.base64Encode(tokenSecret))
                .parseClaimsJws(token)
                .getBody();
    }

    private String getTokenKey(String uuid) {
        return CACHE_TOKEN_PREFIX + uuid;
    }

    private String getRefreshTokenKey(String uuid) {
        return CACHE_TOKEN_REFRESH_PREFIX + uuid;
    }
}
